package torres.map;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import torres.action.Effect;
import torres.ai.AI;
import torres.ai.PassiveAI;
import torres.entity.Avatar;
import torres.entity.Entity;
import torres.entity.Merchant;
import torres.entity.NPC;
import torres.fileIO.ClassContainer;
import torres.fileIO.BasicContainer;
import torres.fileIO.PropertyContainer;
import torres.fileIO.Saveable;
import torres.item.Item;
import torres.map.areaeffect.AreaEffect;
import torres.map.areaeffect.Teleport;
import torres.map.decal.Decal;
import torres.map.decal.Portal;
import torres.map.terrain.Terrain;
import torres.item.ItemManager;
import torres.item.ObstacleItem;
import torres.item.TakeableItem;
import torres.item.TrapItem;

/**
 * @author Torres
 * A collection of tiles that can be added to the game world. These tiles contain
 * terrain, decal, and area effect information. The map also keeps track of the
 * entities and items associated with it.
 */
public class Map implements Saveable {
	
	private Tile[][] tiles;
	private String name;
	private int width;
	private int height;
	
	private HashMap<Location, Entity> entities;
	private HashMap<Location, Item> items;
	private HashMap<Location, TrapItem> traps;
	private HashMap<Location, ObstacleItem> obstacles;
	private HashMap<Location, Effect> effects; 
	private HashMap<String, AI> ai;
	private Collection<Entity> enemies = new ArrayList<Entity>();
	
	/**
	 * Creates a new map
	 */
	public Map() {
		this.entities = new HashMap<Location, Entity>();
		this.items = new HashMap<Location, Item>();
		this.traps = new HashMap<Location, TrapItem>();
		this.obstacles = new HashMap<Location, ObstacleItem>();
		this.effects = new HashMap<Location, Effect>();
		this.ai = new HashMap<String, AI>();
		this.ai.put("passive", new PassiveAI());
	}
	
	/**
	 * @param row the row in the array of tiles
	 * @param col the column in the array of tiles
	 * @param name the name of this map
	 */
	public Map(int row, int col, String name) {
		
		this();
		this.tiles = new Tile[row][col];
		this.name = name;
	}
	
	/**
	 * @param file the map file to read in
	 * @param name the name of this map
	 */
	public Map(File file, String name) {
		
		this();
		this.tiles = MapGenerator.generate(file);
		this.name = name;
		this.width = tiles[0].length;
		this.height = tiles.length;
	}
	
	/**
	 * @param location the location to check for an area effect
	 * @return the area effect at this location
	 */
	public AreaEffect getAreaEffect(Location location) {
		
		return tiles[location.getY()][location.getX()].getAreaEffect();
	}
	
	/**
	 * @param location the location to which an area effect is being applied
	 * @param effect the area effect to apply to the location
	 */
	public void setAreaEffect(Location location, AreaEffect effect) {
		
		tiles[location.getY()][location.getX()].setAreaEffect(effect);
	}
	
	/**
	 * @param location the location to check for an area effect
	 * @return the area effect at the specified location
	 */
	public boolean hasAreaEffect(Location location) {
		
		return getAreaEffect(location) != null;
	}
	
	/**
	 * @param location the location from which to retrieve the decal
	 * @return the decal at the specified location
	 */
	public Decal getDecal(Location location) {
		
		return tiles[location.getY()][location.getX()].getDecal();
	}
	
	/**
	 * @param location the location to whom a decal will be applied
	 * @param decal the decal that is to be applied to the location
	 */
	public void setDecal(Location location, Decal decal) {
		
		tiles[location.getY()][location.getX()].setDecal(decal);
	}
	
	/**
	 * @param location the location to check for a decal
	 * @return whether or not the location has a decal
	 */
	public boolean hasDecal(Location location) {
		
		return getDecal(location) != null;
	}
	
	/**
	 * @param location the location from which to retrieve the terrain
	 * @return the terrain at the specified location
	 */
	public Terrain getTerrain(Location location) {
		
		return tiles[location.getY()][location.getX()].getTerrain();
	}
	
	/**
	 * @param location the location that will have its terrain set
	 * @param terrain the terrain to apply to the location
	 */
	public void setTerrain(Location location, Terrain terrain) {
		tiles[location.getY()][location.getX()].setTerrain(terrain);
	}
	
	/**
	 * @param location the location to check for an entity
	 * @return whether or not there exists an entity at the specified location
	 */
	public boolean hasEntity(Location location) {
		//Tile t = tiles[location.getY()][location.getX()];
		return entities.get(location) != null;
	}
	
	/**
	 * @param location the location from which to get an entity
	 * @return the entity at the specified location
	 */
	public Entity getEntity(Location location) {
		
		return entities.get(location);
	}
	
	/**
	 * @param entity the entity to be added to the map
	 * @param location the location to add the entity to
	 */
	public void addEntity(Entity entity, Location location) {
		
		entities.put(location, entity);
	}
	
	/**
	 * @param location the location from which to remove an entity
	 */
	public void removeEntity(Location location) {
		entities.remove(location);
	}
	
	/**
	 * @param npc the npc to be killed
	 */
	public void killNPC(NPC npc) {
		entities.remove(npc.getLocation());
		enemies.remove(npc);
		
		ai.get(npc.getAIString()).removeEntity(npc);
		
		
		if(this.equals(World.getInstance().getCombatArena())) {
			
			Location location = new Location(5, 5);
			Teleport t = new Teleport("map1", location);
			this.setAreaEffect(location, t);
			this.setDecal(location, Portal.getInstance());
		}
	}
	
	/**
	 * @param location the location to check for an item
	 * @return whether or not the location has an item
	 */
	public boolean hasItem(Location location) {
		return items.get(location) != null;
	}
	
	/**
	 * @param location the location from which to get an item
	 * @return the item at the specified location
	 */
	public Item getItem(Location location) {
		
		return items.get(location);
	}
	
	/**
	 * @param item the item to be added to the map
	 * @param location the location the item is added to
	 */
	public void addItem(Item item, Location location) {
		items.put(location, item);
	}
	
	/**
	 * @param location the location from which to remove an item
	 */
	public void removeItem(Location location) {
		
		items.remove(location);
	}
	
	/**
	 * @param location the location to check for an obstacle
	 * @return whether or not the location has an obstacle
	 */
	public boolean hasObstacle(Location location) {
		return obstacles.get(location) != null;
	}
	
	/**
	 * @param location the location to get an obstacle from
	 * @return the obstacle at the specified location
	 */
	public ObstacleItem getObstacle(Location location) {
		return obstacles.get(location);
	}
	
	/**
	 * @param obstacle the obstacle to be added to the map
	 * @param location the location to add the obstacle to
	 */
	public void addObstacle(ObstacleItem obstacle, Location location) {
		obstacles.put(location, obstacle);
	}
	
	/**
	 * @param location the location from which to remove an obstacle
	 */
	public void removeObstacle(Location location) {
		obstacles.remove(location);
	}
	
	/**
	 * @param location the location to check for a trap
	 * @return whether or not the location has a trap
	 */
	public boolean hasTrap(Location location) {
		return traps.get(location) != null;
	}
	
	/**
	 * @param location the location from which to get a trap
	 * @return the trap at the location
	 */
	public TrapItem getTrap(Location location) {
		return traps.get(location);
	}
	
	/**
	 * @param trap the trap to be added to the map
	 * @param location the location to add the trap to
	 */
	public void addTrap(TrapItem trap, Location location) {
		traps.put(location, trap);
	}
	
	/**
	 * @param location the location from which to remove a trap
	 */
	public void removeTrap(Location location) {
		traps.remove(location);
	}
	
	/**
	 * @param location the location whose validity is in question
	 * @return whether or not the location is valid on the map
	 */
	public boolean isValidLocation(Location location) {
		
		int x = location.getX();
		int y = location.getY();
		
		if(y >= tiles.length || x >= tiles[0].length || y < 0 || x < 0 || this.hasObstacle(location)) {	
			return false;
		}
		return true;
	}
	
	/**
	 * @return the width of the map
	 */
	public int getWidth() {
		
		return width;
	}
	
	/**
	 * @return the height of the map
	 */
	public int getHeight() {
		
		return height;
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		
		return name;
	}

	/**
	 * @param enemy the enemy to be removed from the list of enemies
	 */
	public void removeEnemy(NPC enemy) {
		enemies.remove(enemy);
		ai.get(enemy.getAIString()).removeEntity(enemy);
	}
	
	/**
	 * @param enemy the enemy to add to the list of enemies
	 */
	public void addEnemy(NPC enemy) {
		enemies.add(enemy);
		ai.get(enemy.getAIString()).addEntity(enemy);
	}
	
	/**
	 * @param loc the location to apply the effect to
	 * @param elem the effect to be applied
	 * @param power the power of the effect
	 * @param accuracy the accuracy of the effect
	 */
	public void applyEffect(Location loc, Effect elem, int power, int accuracy) {
		effects.put(loc, elem);
		if(this.hasEntity(loc)) {
			Entity e = this.getEntity(loc);
			e.takeEffect(elem, accuracy, power);
		}
	}
	
	/**
	 * @param loc the location from which to remove an effect
	 */
	public void removeEffect(Location loc) {
		effects.remove(loc);
	}
	
	/**
	 * @param loc the location to check for an effect
	 * @return whether or not the location has an effect
	 */
	public boolean hasEffect(Location loc) {
		return effects.get(loc) != null;
	}
	
	/**
	 * @param loc the location from which to get an effect
	 * @return the effect at the specified location
	 */
	public Effect getEffect(Location loc) {
		return effects.get(loc);
	}
	
	/* (non-Javadoc)
	 * @see torres.fileIO.Saveable#save()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public ClassContainer save() {
		ClassContainer curClass = new ClassContainer("Map");
		curClass.addProperty(new BasicContainer("name", name));
		ClassContainer curStruct = new ClassContainer("Tiles");
		curStruct.addProperty(new BasicContainer("width", tiles.length));
		curStruct.addProperty(new BasicContainer("height", tiles[0].length));
		
		for(int i=0; i < tiles.length; i++) {
			for(int j=0; j < tiles[i].length; j++) {
				curStruct.addProperty(tiles[i][j].save());
			}
		}
		curClass.addProperty(curStruct);
		
		Set<Location> curEntities = entities.keySet();
		Iterator iter = curEntities.iterator();
		ClassContainer curEntityMap = new ClassContainer("Entities");
		ClassContainer curEntityEntry;
		while(iter.hasNext()) {
			curEntityEntry = new ClassContainer("Entry");
			Location curLocation = (Location)iter.next();
			curEntityEntry.addProperty(curLocation.save());
			Entity curEntity = entities.get(curLocation);
			curEntityEntry.addProperty(curEntity.save());
			curEntityMap.addProperty(curEntityEntry);
		}
		curClass.addProperty(curEntityMap);
		
		Set<Location> curItems = items.keySet();
		iter = curItems.iterator();
		ClassContainer curItemMap = new ClassContainer("Items");
		ClassContainer curItemEntry;
		while(iter.hasNext()) {
			curItemEntry = new ClassContainer("Entry");
			Location curLocation = (Location)iter.next();
			curItemEntry.addProperty(curLocation.save());
			Item curItem = items.get(curLocation);
			curItemEntry.addProperty(curItem.save());
			curItemMap.addProperty(curItemEntry);
		}
		curClass.addProperty(curItemMap);
		
		Set<Location> curTraps = traps.keySet();
		iter = curTraps.iterator();
		ClassContainer curTrapMap = new ClassContainer("Traps");
		ClassContainer curTrapEntry;
		while(iter.hasNext()) {
			curTrapEntry = new ClassContainer("Entry");
			Location curLocation = (Location)iter.next();
			curTrapEntry.addProperty(curLocation.save());
			TrapItem curTrap = traps.get(curLocation);
			curTrapEntry.addProperty(curTrap.save());
			curTrapMap.addProperty(curTrapEntry);
		}
		curClass.addProperty(curTrapMap);
		
		Set<Location> curObstacles = obstacles.keySet();
		iter = curObstacles.iterator();
		ClassContainer curObstacleMap = new ClassContainer("Obstacles");
		ClassContainer curObstacleEntry;
		while(iter.hasNext()) {
			curObstacleEntry = new ClassContainer("Entry");
			Location curLocation = (Location)iter.next();
			curObstacleEntry.addProperty(curLocation.save());
			ObstacleItem curObstacle = obstacles.get(curLocation);
			curObstacleEntry.addProperty(curObstacle.save());
			curObstacleMap.addProperty(curObstacleEntry);
		}
		curClass.addProperty(curObstacleMap);
		
		Set<Location> curEffects = effects.keySet();
		iter = curEffects.iterator();
		ClassContainer curEffectMap = new ClassContainer("Effects");
		ClassContainer curEffectEntry;
		while(iter.hasNext()) {
			curEffectEntry = new ClassContainer("Entry");
			Location curLocation = (Location)iter.next();
			curEffectEntry.addProperty(curLocation.save());
			Effect curEffect = effects.get(curLocation);
			curEffectEntry.addProperty(new BasicContainer("Effect", curEffect));
			curEffectMap.addProperty(curEffectEntry);
		}
		curClass.addProperty(curEffectMap);
		
		Set<String> curAIs = ai.keySet();
		iter = curAIs.iterator();
		ClassContainer curAIMap = new ClassContainer("AIs");
		ClassContainer curAIEntry;
		while(iter.hasNext()) {
			curAIEntry = new ClassContainer("Entry");
			String curString = (String)iter.next();
			curAIEntry.addProperty(new BasicContainer("name", curString));
			AI curAI = ai.get(curString);
			curAIEntry.addProperty(curAI.save());
			curAIMap.addProperty(curAIEntry);
		}
		curClass.addProperty(curAIMap);
		
		ClassContainer curEnemies = new ClassContainer("Enemies");
		for(Entity enemy: enemies) {
			curEnemies.addProperty(enemy.save());
		}
		curClass.addProperty(curEnemies);
		
		return curClass;
	}
	
	/* (non-Javadoc)
	 * @see torres.fileIO.Saveable#load(torres.fileIO.ClassContainer)
	 */
	@Override
	public void load(ClassContainer classContainer) {
		PropertyContainer property;
		int i=0;
		//Avert ye virgin eyes
		ClassContainer tileStruct = (ClassContainer)classContainer.getProperty("Tiles");
		BasicContainer heightCont = (BasicContainer)tileStruct.getProperty("height");
		height = Integer.parseInt((String)heightCont.getValue());
		BasicContainer widthCont = (BasicContainer)tileStruct.getProperty("width");
		width = Integer.parseInt((String)widthCont.getValue());
		
		tiles = new Tile[width][height];
		while((property = tileStruct.getProperty("Tile", i))!=null) {
			Tile curTile = new Tile();
			curTile.load((ClassContainer)property);
			tiles[i/width][i%height] = curTile;
			i++;
		}
		entities.clear();
		i=0;
		ClassContainer entryCont;
		ClassContainer entryKey;
		ClassContainer entryValue;
		ClassContainer entityCont = (ClassContainer)classContainer.getProperty("Entities");
		while((property = entityCont.getProperty("Entry", i))!=null) {
			entryCont = (ClassContainer)property;
			entryKey = (ClassContainer)entryCont.getProperty("Location");
			Location location = new Location(0,0);
			location.load(entryKey);
			Entity entity;
			if((entryValue = (ClassContainer)entryCont.getProperty("NPC"))!=null)
				entity = new NPC();
			else if((entryValue = (ClassContainer)entryCont.getProperty("Merchant"))!=null)  {
				List<TakeableItem> items = new ArrayList<TakeableItem>();
				entity = new Merchant(items);
			}
			else {
				entryValue = (ClassContainer)entryCont.getProperty("Avatar");
				entity = new Avatar();
			}
			entity.load(entryValue);
			entities.put(location, entity);
			i++;
		}
		
		items.clear();
		i=0;
		ClassContainer itemCont = (ClassContainer)classContainer.getProperty("Items");
		while((property = itemCont.getProperty("Entry", i))!=null) {
			entryCont = (ClassContainer)property;
			entryKey = (ClassContainer)entryCont.getProperty("Location");
			Location location = new Location(0,0);
			location.load(entryKey);
			entryValue = (ClassContainer)entryCont.getProperty("Item");
			BasicContainer itemIDCont = (BasicContainer)entryValue.getProperty("id");
			int itemID = Integer.parseInt((String)itemIDCont.getValue());
			BasicContainer itemTypeCont = (BasicContainer) entryValue.getProperty("type");
			String itemType = (String)itemTypeCont.getValue();
			Item curItem;
			if(itemType.equals("InteractiveItem"))
				curItem = ItemManager.getInstance().getInteractiveItem(itemID);
			else if(itemType.equals("OneShotItem"))
				curItem = ItemManager.getInstance().getOneShotItem(itemID);
			else if(entryValue.getProperty("type",2)!=null)
				curItem = ItemManager.getInstance().getWeapon(itemID);
			else if(entryValue.getProperty("type",1)!=null)
				curItem = ItemManager.getInstance().getEquippableItem(itemID);
			else if(itemType.equals("ObstacleItem"))
				curItem = ItemManager.getInstance().getObstacleItem(itemID);
			else if(itemType.equals("TakeableItem"))
				curItem = ItemManager.getInstance().getTakeableItem(itemID);
			else if(itemType.equals("TrapItem"))
				curItem = ItemManager.getInstance().getTrapItem(itemID);
			else
				curItem = null;
			items.put(location, curItem);
			i++;
		}
		
		traps.clear();
		i=0;
		ClassContainer trapCont = (ClassContainer)classContainer.getProperty("Traps");
		while((property = trapCont.getProperty("Entry", i))!=null) {
			entryCont = (ClassContainer)property;
			entryKey = (ClassContainer)entryCont.getProperty("Location");
			Location location = new Location(0,0);
			location.load(entryKey);
			entryValue = (ClassContainer)entryCont.getProperty("Item");
			BasicContainer trapID = (BasicContainer)entryValue.getProperty("id");
			TrapItem trap = ItemManager.getInstance().getTrapItem(Integer.parseInt((String)trapID.getValue()));
			traps.put(location, trap);
			i++;
		}
		
		obstacles.clear();
		i=0;
		ClassContainer obstacleCont = (ClassContainer)classContainer.getProperty("Obstacles");
		while((property = obstacleCont.getProperty("Entry", i))!=null) {
			entryCont = (ClassContainer)property;
			entryKey = (ClassContainer)entryCont.getProperty("Location");
			Location location = new Location(0,0);
			location.load(entryKey);
			entryValue = (ClassContainer)entryCont.getProperty("Item");
			BasicContainer obstacleID = (BasicContainer)entryValue.getProperty("id");
			ObstacleItem obstacle = ItemManager.getInstance().getObstacleItem(Integer.parseInt((String)obstacleID.getValue()));
			obstacles.put(location, obstacle);
			i++;
		}
		
		effects.clear();
		i=0;
		ClassContainer effectCont = (ClassContainer)classContainer.getProperty("Effects");
		while((property = effectCont.getProperty("Effect", i))!=null) {
			entryCont = (ClassContainer)property;
			entryKey = (ClassContainer)entryCont.getProperty("Location");
			Location location = new Location(0,0);
			location.load(entryKey);
			BasicContainer entryValueB = (BasicContainer)entryCont.getProperty("Effect");
			Effect effect = Effect.valueOf((String)entryValueB.getValue());
			effects.put(location, effect);
			i++;
		}
		
		ai.clear();
		i=0;
		ClassContainer aiCont = (ClassContainer)classContainer.getProperty("AIs");
		while((property = aiCont.getProperty("Entry", i))!=null) {
			entryCont = (ClassContainer)property;
			BasicContainer entryKeyB = (BasicContainer)entryCont.getProperty("name");
			String name = (String)entryKeyB.getValue();
			entryValue = (ClassContainer)entryCont.getProperty("PassiveAI");
			AI curAI = new PassiveAI();
			curAI.load(entryValue);
			ai.put(name, curAI);
			i++;
		}
		
		ClassContainer enemiesStruct = (ClassContainer)classContainer.getProperty("Enemies");
		i=0;
		enemies.clear();
		while((property = enemiesStruct.getProperty("NPC", i))!=null) {
			NPC curNPC = new NPC();
			curNPC.load((ClassContainer)property);
			enemies.add(curNPC);
			i++;
		}
		
		BasicContainer nameCont = (BasicContainer)classContainer.getProperty("name");
		name = (String)nameCont.getValue();
		
	}

	/**
	 * @param enemies the enemies to set
	 */
	public void setEnemies(Collection<Entity> enemies) {
		this.enemies = enemies;
	}

	/**
	 * @return the enemies
	 */
	public Collection<Entity> getEnemies() {
		return enemies;
	}
	
	/**
	 * @param aiString the string by which to identify the ai
	 * @param e the entity to register
	 */
	public void registerEnemyAI(String aiString, NPC e) {
		AI ai  = this.ai.get(aiString);
		ai.addEntity(e);
	}
	
	/**
	 * run the ai
	 */
	public void runAI() {
		for(AI ai : this.ai.values()) {
			ai.runAll();
		}
	}
}
